提問: 消費者從商品明細頁回到商品列表的時候,希望可以保留消費者先前瀏覽的位置。
我們可以透過頁面緩存機制來實現這個需求。
簡單來說,頁面緩存的工作原理是,當用戶第一次訪問某個頁面時,系統會把這個頁面的內容保存到緩存中。這就像把食物放進冰箱一樣,方便以後取用。當同一位用戶或其他用戶再次訪問這個頁面時,系統不需要再去伺服器請求新的內容,而是直接從緩存中提取已經保存的網頁內容。這樣能大大提高加載速度,因為從緩存中取資料比從伺服器獲取要快得多。
那麼緩存該存在哪裡呢?
頁面緩存可以儲存在多個地方,並且每種儲存方式都有其特點和適用場景。分享一些常見的儲存位置:
內存(
Memory)
頁面數據直接儲存在應用的內存中,通常使用狀態管理庫(如NgRx、Redux、Vuex)來管理。
優點是速度非常快,因為數據就存在應用程式的內存中,且也容易對應頻繁的數據變更。
但缺點是當應用程式生命週期結束時,緩存也會隨之丟失,因此不適合用來儲存需要長期保留的資訊(例如:使用者設定、登入狀態等)。
LocalStorage
瀏覽器提供的一種持久化存儲方式,可以將資料透過key-value的方式進行儲存,且資料在瀏覽器關閉後仍然存在。
優點是資料可以在跨生命週期的瀏覽中都能夠存取,而且是瀏覽器提供的API,容易使用。
不過缺點也很明顯,除了有容量限制外,也不適合存放敏感資料,因為可以透過瀏覽器的開發者工具一目了然。
SessionStorage
也是瀏覽器提供的儲存方式,不過資料僅在當前瀏覽器會話中有效,瀏覽器關閉後數據就會消失。
優缺點與LocalStorage相同,特性上更適合用來儲存只在當次瀏覽器生命週期中所需要的資料。
這次我們選擇使用 Angular 並搭配 ngRx 來實現瀏覽位置和資料的緩存,因為當使用者離開應用再次訪問時,就不需要重新快取了。
設計理念如下:
首先透過 interface 來規範緩存的資料和規格,再來在 reducer 中加入一個 state 負責管理緩存狀態,action 負責更新緩存。
需要緩存的頁面,可以在 OnInit life hook 的時候判斷是要使用緩存還是載入資料,並在 OnDestroy life hook 的時候將使用者最後瀏覽的狀態更新到 store 中。
最後若有外部呼叫需要手動清除緩存,也能透過呼叫 action 來完成。
我們來看看程式碼。
export interface IComponentCache {
  //#region 資料
  products: Array<Product>;
  //#endregion 資料
  //#region 使用者操作狀態類
  // 瀏覽頁數
  pageCnt: number;
  // 複數資料瀏覽位置
  multiItemScrolledRecord: Map<string, number>;
  // 單一資料瀏覽位置
  itemScrolledRecord: number;
  //#endregion 使用者操作狀態類
}
首先是 interface 的部分,主要將裡面的 property 分成資料和定位兩部分。值得一提的是,定位我們可以透過 Map 型別來儲存多筆資料,並使用 ID 進行管理。
/**
 * 緩存 store
 */
export const CacheActions = createActionGroup({
  source: 'CACHE',
  events: {
    UPDATE_CACHE: props<{
      cache: IComponentCache | null;
    }>(),
  },
});
/**
 * STATE 初始狀態
 */
export const initialState: ICacheState = {
  componentCache: null,
};
/**
 * STATE 宣告
 */
export interface ICacheState {
  componentCache: IComponentCache | null;
}
/**
 * REDUCER 宣告
 */
export const cacheReducer = createReducer(
  initialState,
  on(CacheActions.UPDATE_CACHE, (state, action) => {
    return {
      ...state,
      componentCache: action.cache,
    };
  }),
);
/**
 * selector 宣告
 */
export const selectComponentCache = createSelector(
  selectCache,
  (state: ICacheState) => state.componentCache
);
Store 的部分 componentCache 可以是 null 或是物件,透過這個方式來判斷是否載入緩存。
ngOnInit(): void {
  this.store
    .select(selectComponentCache)
    .pipe(take(1))
    .subscribe((cache) => {
      if (cache) {
        // 快取還原處理
        this.isCache = true;
      } else {
        // 初始化
        this.isCache = false;
      }
      // 無論緩存與否都要的流程
    });
}
ngAfterViewInit(): void {
  if (this.isCache) {
    // 還原滾動位置
  }
}
ngOnDestroy(): void {
  this.store.dispatch(
    CacheActions.UPDATE_CACHE({
      componentCache: {
        // 要快取的資料
      },
    })
  );
}
最後是緩存 component 的實作,值得一提的是,在 ngAfterViewInit 的時候我們才還原滾動位置,這樣可以確保使用 viewChild 或是 viewportScroller 的時候能抓取到正確的定位和 DOM,避免尚未渲染完畢就進行滾動。
以上就是本次緩存實作的分享!下一篇文章會分享如何實作全域的動態 NavBar。
圖片出處: https://medium.com/@mena.meseha/3-major-problems-and-solutions-in-the-cache-world-155ecae41d4f